package dao;

import db.ConnectionFactory;
import exception.DAOException;
import to.Drug;

import java.sql.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Deque;
import java.util.List;

/**
 * Класс инкапсулирует методы работы c БД для объектов типа Drug.
 * СУБД - MySQL.
 *
 * @author Andrey Murgo
 */
public class DrugDAO {

    /**
     * Используюется для орагнизации постраничного просмотра результатов.
     * Сохраняет общее количество подходящих записей в базе после вызова методов,
     * использующих постраничный доступ (getDrugs)
     */
    private int numberOfRecordsTotal;

    private static Connection getConnection() {
        try {
            return ConnectionFactory.getInstance().getConnection();
        } catch (SQLException e) {
            throw new DAOException("Не удалось установить соединение с базой. Причина:" + e.getMessage(), e);
        }
    }

    public int getNumberOfRecordsTotal() {
        return numberOfRecordsTotal;
    }

    /**
     * Получение записей из таблицы лекарства.
     * Все параметры не обязательны.
     *
     * @param offset      Смещение от начала (количество пропущенных записей) (для постраничного доступа)
     * @param noOfRecords Количество записей в результате(для постраничного доступа)
     * @param drugParam   Параметры лекартсва для выборки
     * @return Результат поиска - коллекция объектов Drug
     * @throws DAOException
     */
    public Collection<Drug> getDrugs(Integer offset, Integer noOfRecords, DrugParam drugParam) {
        try (Connection c = getConnection()) {
            try (Statement stmt = c.createStatement()) {
                //Сборка запроса для БД
                StringBuilder queryBuilder = new StringBuilder("SELECT * FROM drugs ");  //
                String whereCondition = "";
                if (drugParam != null) {
                    whereCondition = createDrugWhereCondition(drugParam);
                    queryBuilder.append(whereCondition);
                    queryBuilder.append(" ORDER BY DRUG_NAME");
                }
                /*
                Смещение без указания ограничения количества записей не поддерживается.
                Решение из документации MySQL:
                To retrieve all rows from a certain offset up to the end of the result set,
                you can use some large number for the second parameter.
                This statement retrieves all rows from the 96th row to the last:
                SELECT * FROM tbl LIMIT 95,18446744073709551615;
                 */
                if ((offset != null) || (noOfRecords != null)) {
                    queryBuilder.append(" LIMIT ");
                    if (offset != null) {
                        queryBuilder.append(offset).append(",");
                        if (noOfRecords != null) {
                            queryBuilder.append(noOfRecords);
                        } else {
                            queryBuilder.append(Long.MAX_VALUE);
                        }
                    } else {   //offset = null, noOfRecords != null
                        queryBuilder.append(noOfRecords);
                    }
                }
                ResultSet rs = stmt.executeQuery(queryBuilder.toString());
                Collection<Drug> result = getDrugsFromResultSet(rs);
                rs.close();

                rs = stmt.executeQuery("SELECT COUNT(*) FROM  drugs " + whereCondition);
                if (rs.next()) {
                    numberOfRecordsTotal = rs.getInt(1);
                }
                return result;
            }
        } catch (SQLException e) {
            throw new DAOException("Не удалось получить лекарства из базы. Причина:" + e.getMessage(), e);
        }
    }

    /**
     * Используется для получения всех противопоказаний (записей из таблицы contraindication_list)
     *
     * @return Коллекция противопоказаний
     * @throws DAOException
     */
    public Collection<Drug.ContraIndication> getAllContraIndications() {
        try (Connection c = getConnection()) {
            try (Statement stmt = c.createStatement()) {
                Collection<Drug.ContraIndication> result = new ArrayList<>();
                ResultSet rs = stmt.executeQuery("SELECT * FROM contraindication_list");
                while (rs.next()) {
                    result.add(new Drug.ContraIndication(
                            Integer.parseInt(rs.getString("CONTRAINDICATION_ID")),
                            rs.getString("CONTRAINDICATION_NAME")
                    ));
                }
                return result;
            }
        } catch (SQLException e) {
            throw new DAOException("Не удалось получить противопоказания из базы. Причина:" + e.getMessage(), e);
        }
    }

    /**
     * Используется для получения лекарст, несовеместимых с данным
     *
     * @param drugId Id лекарства в БД, для которого нужно получить список противопоказаний
     * @return Коллекция объектов Drug с несовместимыми лекарствами
     * @throws DAOException
     */
    public Collection<Drug> getIncompatibilities(int drugId) {
        try (Connection c = getConnection()) {
            try (Statement stmt = c.createStatement()) {
                ResultSet rs = stmt.executeQuery(
                        "SELECT d2.DRUG_ID, d2.DRUG_NAME, d2.SUBSTANCE, d2.FARMA_ACTION, d2.USEFUL_MONTH, d2.SALES_TERMS, d2.COST " +
                                "FROM incompatibility i " +
                                "INNER JOIN drugs d ON (d.DRUG_ID = i.DRUG_ID1) " +
                                "INNER JOIN drugs d2 ON (d2.DRUG_ID = i.DRUG_ID2) " +
                                "WHERE d.DRUG_ID = " + drugId +
                                " UNION " +
                                "SELECT d2.DRUG_ID, d2.DRUG_NAME, d2.SUBSTANCE, d2.FARMA_ACTION, d2.USEFUL_MONTH, d2.SALES_TERMS, d2.COST " +
                                "FROM incompatibility i " +
                                "INNER JOIN drugs d ON (d.DRUG_ID = i.DRUG_ID2) " +
                                "INNER JOIN drugs d2 ON (d2.DRUG_ID = i.DRUG_ID1) " +
                                "WHERE d.DRUG_ID = " + drugId + ";");
                return getDrugsFromResultSet(rs);
            }
        } catch (SQLException e) {
            throw new DAOException("Не удалось получить несовместимые лекарства из базы. Причина:" + e.getMessage(), e);
        }
    }

    /**
     * Используется для получения лекарств, аналогичных данному
     *
     * @param drugId Id лекарства в БД, для которого нужно получить список аналогов
     * @return Коллекция объектов Drug с аналогичными лекарствами
     * @throws DAOException
     */
    public Collection<Drug> getAnalogs(int drugId) {
        try (Connection c = getConnection()) {
            try (Statement stmt = c.createStatement()) {
                String query = "SELECT d2.DRUG_ID, d2.DRUG_NAME, d2.SUBSTANCE, d2.FARMA_ACTION, d2.USEFUL_MONTH, d2.SALES_TERMS, d2.COST " +
                        "FROM analogs a " +
                        "INNER JOIN drugs d ON (d.DRUG_ID = a.DRUG_ID1) " +
                        "INNER JOIN drugs d2 ON (d2.DRUG_ID = a.DRUG_ID2) " +
                        "WHERE d.DRUG_ID = " + drugId +
                        " UNION " +
                        "SELECT d2.DRUG_ID, d2.DRUG_NAME, d2.SUBSTANCE, d2.FARMA_ACTION, d2.USEFUL_MONTH, d2.SALES_TERMS, d2.COST " +
                        "FROM analogs a " +
                        "INNER JOIN drugs d ON (d.DRUG_ID = a.DRUG_ID2) " +
                        "INNER JOIN drugs d2 ON (d2.DRUG_ID = a.DRUG_ID1) " +
                        "WHERE d.DRUG_ID = " + drugId + ";";
                ResultSet rs = stmt.executeQuery(query);
                return getDrugsFromResultSet(rs);
            }
        } catch (SQLException e) {
            throw new DAOException("Не удалось получить аналоги из базы. Причина:" + e.getMessage(), e);
        }
    }

    /**
     * Используется для получнеия списка противопоказаний для данного лекарства
     *
     * @param drugId Id лекарства в БД, для которого нужно получить список противопоказаний
     * @return Коллекция объектов Drug.ContraIndication с противопоказаниями
     * @throws DAOException
     */
    public Collection<Drug.ContraIndication> getContraIndications(int drugId) {
        Collection<Drug.ContraIndication> result = new ArrayList<>();
        try (Connection c = getConnection()) {
            try (Statement stmt = c.createStatement()) {
                ResultSet rs = stmt.executeQuery(
                        "SELECT cl.CONTRAINDICATION_ID, cl.CONTRAINDICATION_NAME\n" +
                                "FROM  drugs d " +
                                "NATURAL JOIN contraindication ci " +
                                "NATURAL JOIN contraindication_list cl " +
                                "WHERE d.drug_id = " + drugId + ";"
                );
                while (rs.next()) {
                    result.add(new Drug.ContraIndication(
                            Integer.parseInt(rs.getString("CONTRAINDICATION_ID")),
                            rs.getString("CONTRAINDICATION_NAME")
                    ));
                }
            }
        } catch (SQLException e) {
            throw new DAOException("Не удалось получить противопоказания для лекарства. Причина:" + e.getMessage(), e);
        }
        return result;
    }

    /**
     * Добавление лекарства в БД
     *
     * @param drug Объект Drug, содержащий информацию о добавляемом лекарстве
     * @return ID лекраства. Внимание! Получение ID может не поддерживаться некоторыми драйверами JDBC!
     * @throws DAOException
     */
    public int addDrug(Drug drug) {
        try (Connection c = getConnection()) {
            String query = "INSERT INTO drugs(DRUG_NAME, SUBSTANCE, FARMA_ACTION, USEFUL_MONTH, SALES_TERMS, COST) " +
                    "VALUES(?,?,?,?,?,?)";
            try (PreparedStatement pstmt = c.prepareStatement(query, Statement.RETURN_GENERATED_KEYS)) {
                pstmt.setString(1, drug.getDrugName());
                pstmt.setString(2, drug.getSubstance());
                pstmt.setString(3, drug.getFarmaAction());
                pstmt.setString(4, Integer.toString(drug.getUsefulMonth()));
                pstmt.setString(5, drug.getSalesTerms());
                pstmt.setString(6, Double.toString(drug.getCost()));
                if (pstmt.executeUpdate() == 0) {
                    throw new DAOException("Не удалось добавить лекарство");
                }
                ResultSet generatedKeys = pstmt.getGeneratedKeys();
                if (generatedKeys.next()) {
                    return generatedKeys.getInt(1);
                } else {
                    throw new DAOException("Не удалось добавить лекарство, сгенерированный ID не получен");
                }
            }
        } catch (SQLException e) {
            throw new DAOException("Не удалось добавить лекарство в базу. Причина:" + e.getMessage(), e);
        }
    }

    /**
     * Добавление аналога в БД
     *
     * @param drugId    Id лекарства в БД, для которого требуется добавить аналог
     * @param analogsId Id аналогичного лекарства в БД
     * @throws DAOException
     */
    public void addAnalogs(int drugId, Collection<Integer> analogsId) {
        try (Connection c = getConnection()) {
            String query = "INSERT INTO analogs(DRUG_ID1, DRUG_ID2) VALUES(?,?)";
            for (int analogId : analogsId) {
                try (PreparedStatement pstmt = c.prepareStatement(query)) {
                    pstmt.setString(1, Integer.toString(drugId));
                    pstmt.setString(2, Integer.toString(analogId));
                    if (pstmt.executeUpdate() == 0) {
                        throw new DAOException("Не удалось добавить аналог с id " + analogId);
                    }
                }
            }
        } catch (SQLException e) {
            throw new DAOException("Не удалось добавить аналоги лекартсва в базу. Причина:" + e.getMessage(), e);
        }
    }

    /**
     * Добавление несовместимых лекарств для указанного
     *
     * @param drugId            Id лекарства в БД, для которого требуется добавить несовемстимости
     * @param incompatibilities Коллекция, содержащяя Id несовемстимых лекарств
     * @throws DAOException
     */
    public void addIncompatibilities(int drugId, Collection<Integer> incompatibilities) {
        try (Connection c = getConnection()) {
            String query = "INSERT INTO incompatibility(DRUG_ID1, DRUG_ID2) VALUES(?,?)";
            for (int incompId : incompatibilities) {
                try (PreparedStatement pstmt = c.prepareStatement(query)) {
                    pstmt.setString(1, Integer.toString(drugId));
                    pstmt.setString(2, Integer.toString(incompId));
                    if (pstmt.executeUpdate() == 0) {
                        throw new DAOException("Не удалось добавить несовместимость с id " + incompId);
                    }
                }
            }
        } catch (SQLException e) {
            throw new DAOException("Не удалось добавить несовместимые лекарства в базу. Причина:" + e.getMessage(), e);
        }
    }

    /**
     * Добавление противопоказаний для указанного лекарства
     *
     * @param drugId            Id лекарства в БД, для которого требуется добавить противопоказания
     * @param contraindications Коллекция, содержащяя Id противопоказаний
     * @throws DAOException
     */
    public void addContraindications(int drugId, Collection<Integer> contraindications) {
        try (Connection c = getConnection()) {
            String query = "INSERT INTO contraindication(DRUG_ID, CONTRAINDICATION_ID) VALUES(?,?)";
            for (int contraId : contraindications) {
                try (PreparedStatement pstmt = c.prepareStatement(query)) {
                    pstmt.setString(1, Integer.toString(drugId));
                    pstmt.setString(2, Integer.toString(contraId));
                    if (pstmt.executeUpdate() == 0) {
                        throw new DAOException("Не удалось добавить противопоказание с id " + contraId);
                    }
                }
            }
        } catch (SQLException e) {
            throw new DAOException("Не удалось добавить противопоказание для лекарства в базу. Причина:" + e.getMessage(), e);
        }
    }

    /**
     * Удаление лекарства из БД. Данные удаляются также и из связанных таблиц
     *
     * @param id Id удаляемого лекарства
     * @throws DAOException
     */
    public void deleteDrug(int id) {
        try (Connection c = getConnection()) {
            //Удаление из связанных таблиц
            String query = "";
            try (Statement stmt = c.createStatement()) {
                try {
                    query = "DELETE FROM analogs WHERE (DRUG_ID1=" + id + " OR DRUG_ID2=" + id + ")";
                    stmt.executeUpdate(query);
                } catch (SQLException e) {
                    throw new DAOException("Не удалось удалить лекартсво из базы. Причина:" + e.getMessage() + query, e);
                }
            }
            try (Statement stmt = c.createStatement()) {
                try {
                    query = "DELETE FROM incompatibility WHERE (DRUG_ID1=" + id + " OR DRUG_ID2=" + id + ")";
                    stmt.executeUpdate(query);
                } catch (SQLException e) {
                    throw new DAOException("Не удалось удалить лекартсво из базы. Причина:" + e.getMessage() + query, e);
                }
            }
            try (Statement stmt = c.createStatement()) {
                try {
                    query = "DELETE FROM contraindication WHERE (DRUG_ID=" + id + ")";
                    stmt.executeUpdate(query);
                } catch (SQLException e) {
                    throw new DAOException("Не удалось удалить лекартсво из базы. Причина:" + e.getMessage() + query, e);
                }
            }
            //Удаление из основной таблицы
            try (Statement stmt = c.createStatement()) {
                try {
                    query = "DELETE FROM drugs WHERE DRUG_ID=" + id;
                    stmt.executeUpdate(query);
                } catch (SQLException e) {
                    throw new DAOException("Не удалось удалить лекартсво из базы. Причина:" + e.getMessage() + query, e);
                }
            }
        } catch (SQLException e) {
            throw new DAOException("Не удалось удалить лекартсво из базы. Причина:" + e.getMessage(), e);
        }
    }

    /**
     * Создает строку-условие WHERE на основе переданных данных
     * для таблицы DRUGS
     */
    private String createDrugWhereCondition(DrugParam drugParam) {
        String result = "";

        Integer id = drugParam.getId();
        String drugName = drugParam.getDrugName();
        String substance = drugParam.getSubstance();
        String farmaaction = drugParam.getFarmaaction();
        Double minCost = drugParam.getMinCost();
        Double maxCost = drugParam.getMaxCost();

        List<String> conditionParts = new ArrayList<>();
        if (id != null) {
            conditionParts.add("DRUG_ID = " + id);
            //Т.к. id однозначно определяет запись в таблице, остальные параметры не важны
        } else {
            //Чтобы поиск не был чувствителен к регистру преобразуем всё к верхнему, функцией UPPER
            if (drugName != null) {
                conditionParts.add("UPPER(DRUG_NAME) LIKE UPPER('%" + drugName +"%')");
            }
            if (substance != null) {
                conditionParts.add("UPPER(SUBSTANCE) LIKE UPPER('%" + substance +"%')");
            }
            if (farmaaction != null) {
                conditionParts.add("UPPER(FARMA_ACTION) LIKE UPPER('%" + farmaaction +"%')");
            }
            if (minCost != null) {
                conditionParts.add("COST > " + minCost);
            }
            if (maxCost != null) {
                conditionParts.add("COST < " + maxCost);
            }
        }

        if(!conditionParts.isEmpty()) {
            StringBuilder sb = new StringBuilder("WHERE ( ");
            for (int i = 0; i < conditionParts.size(); i++) {
                sb.append(conditionParts.get(i)).append(" ");
                if (i != (conditionParts.size() - 1)) {
                    sb.append("AND ");
                }
            }
            sb.append(")");
            result = sb.toString();
        }
        return result;
    }

    /**
     * Преобразует результаты полученные из БД в коллекцию объектов Drug
     *
     * @param rs Объект ResultSet
     * @return Коллекция объектов Drug
     * @throws SQLException
     */
    private Collection<Drug> getDrugsFromResultSet(ResultSet rs) throws SQLException {
        Collection<Drug> result = new ArrayList<>();
        while (rs.next()) {
            result.add(new Drug(
                    Integer.parseInt(rs.getString("DRUG_ID")),
                    rs.getString("DRUG_NAME"),
                    rs.getString("SUBSTANCE"),
                    rs.getString("FARMA_ACTION"),
                    Integer.parseInt(rs.getString("USEFUL_MONTH")),
                    rs.getString("SALES_TERMS"),
                    Double.parseDouble(rs.getString("COST"))
            ));
        }
        return result;
    }

    /**
     * Класс инкапсулирует параметры, по которым проводится
     * выборка лекарств из базы
     */
    public static class DrugParam {
        private Integer id;
        private String drugName;
        private String substance;
        private String farmaaction;
        private Double minCost;
        private Double maxCost;

        public Integer getId() {
            return id;
        }

        public void setId(Integer id) {
            this.id = id;
        }

        public String getDrugName() {
            return drugName;
        }

        public void setDrugName(String drugName) {
            this.drugName = drugName;
        }

        public String getSubstance() {
            return substance;
        }

        public void setSubstance(String substance) {
            this.substance = substance;
        }

        public String getFarmaaction() {
            return farmaaction;
        }

        public void setFarmaaction(String farmaaction) {
            this.farmaaction = farmaaction;
        }

        public Double getMinCost() {
            return minCost;
        }

        public void setMinCost(Double minCost) {
            this.minCost = minCost;
        }

        public Double getMaxCost() {
            return maxCost;
        }

        public void setMaxCost(Double maxCost) {
            this.maxCost = maxCost;
        }
    }
}
